/*******************************************************************************

    copyright:  Copyright (c) 2007 Matthias Walter. All rights reserved

    license:    BSD style: $(LICENSE)

    version:    Initial release: May 2007

    author:     Matthias Walter

*******************************************************************************/

module fcgi.Request;

private import  fcgi.Base;
private import  fcgi.Conduit;
private import  fcgi.Connection;
private import  fcgi.InternalConduit;
private import  fcgi.Protocol;
private import  fcgi.Exception;

private import  tango.io.Console;
private import  tango.math.Math;
private import  mango.io.protocol.NativeProtocol;
private import  mango.io.protocol.Reader;
private import  tango.sys.Environment;

public import fcgi.Buffer;

class FastCGIRequest
{
    private static FastCGIConnection globalConnection_ = null;

    public int exitStatus;
    
    private int id_;
    private FastCGIRole role_;
    private bool keepAlive_;
    private bool usedStderr_;
    private bool withExceptions_;

    private FastCGIConnection connection_;
    private int fileDescriptor_;
    private FastCGIInternalConduit conduit_;
    private Reader reader_;
    private NativeProtocol fcgi_protocol_;

    private FastCGIConduit[FastCGIStreamType] stream_;
    private char[][char[]] arguments_;

    public this (FastCGIConnection connection = null, bool withExceptions = false)
    {
        // connection passed?
        synchronized
        {
            if (connection is null)
            {
                if (globalConnection_ is null)
                    globalConnection_ = new FastCGIConnection ();
                connection = globalConnection_;
            }
        }

        // init some variables
        connection_ = connection;
        fileDescriptor_ = -1;
        conduit_ = new FastCGIInternalConduit ();
        fcgi_protocol_ = new NativeProtocol(conduit_, false);
        reader_ = new Reader (fcgi_protocol_);
        exitStatus = 0;
        id_ = -1;
        role_ = FastCGIRole.UnknownRole;
        conduit_.finish ();
        arguments_ = arguments_.init;
        withExceptions_ = withExceptions;

        // create streams
        stream_[FastCGIStreamType.Data] = new FastCGIConduit (this, FastCGIStreamType.Data, true);
        stream_[FastCGIStreamType.Stdin] = new FastCGIConduit (this, FastCGIStreamType.Stdin, true);
        stream_[FastCGIStreamType.Stdout] = new FastCGIConduit (this, FastCGIStreamType.Stdout, false);
        stream_[FastCGIStreamType.Stderr] = new FastCGIConduit (this, FastCGIStreamType.Stderr, false);
    }

    public bool accept ()
    {
        if (!isFinished ())
            finish ();

        // accept from connection
        fileDescriptor_ = connection_.accept ();

        // set internal conduit's fd
        conduit_.init (fileDescriptor_);

        // reset four streams
        foreach (type, stream; stream_)
            stream.reset ();

        if (connection_.isCGI)
        {
            // first record, always responder
            id_ = 1;
            role_ = FastCGIRole.Responder;
            // set data stream to eof - the others won't be used
            stream_[FastCGIStreamType.Stderr].setEndOfFile ();
            arguments_ = Environment.get ();
            return fileDescriptor_ >= 0;
        }
        else
        {
            // start processing begin records until reached stream
            FastCGIHeader header;

            do
            {
                header = readRecord ();
            }
            while (! header.isInputStream ());

            if (header.contentLength == 0)
            {
                stream_[header.streamType].setEndOfFile ();
            }
            else
            {
                // read data from this record and push it onto the stream
                char[] data = new char[header.contentLength];
                reader_.get (data);
                stream_[header.streamType].pushData (data);
            }


            return true;
        }
    }

    public void finish ()
    {
        if (!connection_.isCGI)
        {
            // eof records
            writeRecord (FastCGIRecordType.Stdout, null, 0);
            if (usedStderr_)
                writeRecord (FastCGIRecordType.Stderr, null, 0);

            // end record
            FastCGIEndRequest content;
            content.appStatus1 = (exitStatus >> 24) & 0xff;
            content.appStatus2 = (exitStatus >> 16) & 0xff;
            content.appStatus3 = (exitStatus >> 8) & 0xff;
            content.appStatus4 = (exitStatus) & 0xff;
            content.protocolStatus = FastCGIProtocolStatus.RequestComplete ;
            content.reserved[0] = 0;
            content.reserved[1] = 0;
            content.reserved[2] = 0;
            writeRecord (FastCGIRecordType.EndRequest, &content, content.sizeof);

            // close connection
            if (!keepAlive_)
            {
                OS_IpcClose (fileDescriptor_);
                conduit_.finish ();
            }

            // reset vars
            exitStatus = 0;
            id_ = -1;
            //reader_.buffer.flush ();
            role_ = FastCGIRole.UnknownRole;
            arguments_ = arguments_.init;
            usedStderr_ = false;
            foreach (stream; stream_)
                stream.reset ();
        }
    }

    package bool isFinished ()
    {
        return id_ < 0;
    }

    public OutputStream stdout ()
    {
        if (connection_.isCGI)
            return Cout.stream;
        else
            return stream_[FastCGIStreamType.Stdout].output;
    }

    public OutputStream stderr ()
    {
        if (connection_.isCGI)
            return Cerr.stream;
        else
            return stream_[FastCGIStreamType.Stderr].output;
    }

    public InputStream stdin ()
    {
        if (connection_.isCGI)
            return Cin.stream;
        else
            return stream_[FastCGIStreamType.Stdin].input;
    }

    public InputStream data ()
    {
        return stream_[FastCGIStreamType.Data].input;
    }

    public char[][char[]] args ()
    {
        return arguments_;
    }

    package uint read (FastCGIStreamType type, char[] data)
    {
        FastCGIHeader header;
        if (stream_[type].isEndOfFile ())
            return 0; 

        while (1)
        {
            // loop through, until we get a stream record
            
            do
            {
                header = readRecord ();
            }
            while (!header.isInputStream ());

            // empty packet = eof
            if (header.contentLength == 0)
                stream_[header.streamType].setEndOfFile();

            int copy = 0;
            if (type == header.streamType)
            {
                copy = min(header.contentLength, data.length);
                // read directly
                if (copy > 0)
                    reader_.get (data); //[0 .. copy]);
            }

            // other stream or rest of this record
            if (header.contentLength > copy)
            {
                char[] temp = new char[header.contentLength - copy];
                reader_.get (temp);
                stream_[header.streamType].pushData(temp);
            }
            header.readPadding (reader_);

            if (type == header.streamType)
                return copy;
        }
    }
    
    package uint write (FastCGIStreamType type, char[] data)
    {
        if (type == FastCGIStreamType.Stderr && data.length > 0)
            usedStderr_ = true;
        return writeRecord (cast(FastCGIRecordType) type, data.ptr, data.length);
    }

    FastCGIInternalConduit conduit ()
    {
        return conduit_;
    }

    public uint id ()
    {
        return id_;
    }
    
    private uint writeRecord (FastCGIRecordType type, void* data, int size)
    {
        FastCGIHeaderStruct header;

        // fill header
        header.protocolVersion = FastCGIVersion;
        header.recordType = type;
        header.reqID1 = (id_ >> 8) & 0xff;
        header.reqID2 = (id_) & 0xff;
        int len = min(size, ushort.max);
        // calc padding
        int padding = 8 - (len & 7);
        if (padding == 8)
                padding = 0;

        header.contentLength1 = (len >> 8) & 0xff;
        header.contentLength2 = (len) & 0xff;
        header.paddingLength = padding;
        header.reserved = 0;

        // create some space for this packet
        char[] buffer = new char[header.sizeof + len + padding];
        // copy header
        buffer[0 .. header.sizeof] = (cast(char*) &header)[0 .. header.sizeof];
        // copy data
        buffer[header.sizeof .. header.sizeof + len] = cast(char[]) data[0 .. len];
        // pad with zeros
        buffer[header.sizeof + len .. header.sizeof + len + padding] = 0;

        conduit_.write(buffer);

        return len;
    }
    
    private FastCGIHeader readRecord ()
    {
        FastCGIHeader header = new FastCGIHeader ();

        reader_.get (header);

        if (! header.checkVersion ())
            throw new FastCGIProtocolException ("Unsupported FastCGI version");
            
        switch (header.recordType)
        {
        case FastCGIRecordType.BeginRequest:
            if (header.requestID == 0)
                throw new FastCGIProtocolException ("FastCGI protocol error: BeginRecord.requestID was 0");

            FastCGIBeginRequest request = new FastCGIBeginRequest ();

            if (header.contentLength != request.length)
                throw new FastCGIProtocolException ("FastCGI protocol error: Content length mismatch");

            if (id_ >= 0)
                throw new FastCGIProtocolException ("FastCGI protocol error: Multiplexed connections are not implemented - please report this bug!");

            id_ = header.requestID;

            reader_.get (request);

            keepAlive_ = request.isKeepAlive;

            header.readPadding (reader_);
        break;

        case FastCGIRecordType.AbortRequest:
            if (withExceptions_)
                throw new FastCGIAbortException ();
        break;

        case FastCGIRecordType.GetValues:
            // empty one
            if (header.contentLength == 0)
                return header;

            int nameLength = readVariableLength (reader_);
            int valueLength = readVariableLength (reader_);

            // name
            char[] name = new char[nameLength];
            reader_.get (name);
            // value
            char[] value = new char[valueLength];
            reader_.get (value);
            // padding
            header.readPadding (reader_);

            char[] answer = "";
            if (name == "FCGI_MAX_CONNS")
                answer = "100";
            else if (name == "FCGI_MAX_REQS")
                answer = "100";
            else if (name == "FCGI_MPXS_CONNS")
                answer = "0";

            // create name/value pair
            char[] buffer;
            writeVariableLength (buffer, name);
            writeVariableLength (buffer, answer);
            // send it
            writeRecord (FastCGIRecordType.GetValuesResult, buffer.ptr, buffer.length);
        break;

        case FastCGIRecordType.Params:
            // empty one
            if (header.contentLength == 0)
                return header;
            int toRead = header.contentLength - header.paddingLength;
            while (toRead > 0)
            {
                int nameLen = readVariableLength (reader_);
                int valueLen = readVariableLength (reader_);

                // name
                char[] name = new char[nameLen];
                reader_.get (name);
                // value
                char[] value = new char[valueLen];
                reader_.get (value);
                // put it
                arguments_[name] = value;
                // decrease toRead
                toRead -= (nameLen < 128 ? nameLen+1 : nameLen+4) + (valueLen < 128 ? valueLen+1 : valueLen+4);
            }
            // padding
            header.readPadding (reader_);
            // sent more than one pair per record?
            if (toRead != 0)
                throw new FastCGIProtocolException ("FastCGI protocol error: Receiving multiple name/value pairs in one record is not implemented");
        break;

        case FastCGIRecordType.Stdin:
        case FastCGIRecordType.Data:
            // do nothing here
        break;

        default:
            throw new FastCGIProtocolException ("FastCGI protocol error: Unknown record type");
        }

        return header;
    }
};
